這篇要來聊聊 Kotlin 在 standard library 中所提供的 Scope Functions,至於什麼是 Scope Functions 以及該如何使用,我們就來一一探討吧!以下如有解釋不清或是描述錯誤的地方還請大家多多指教:
Scope Functions 的目標很單一,透過一個 block 包覆相同物件所執行的內容,並且執行這個 block 裡所有的動作,這些方法有著 lambda 表示式,針對指定的物件形成一個臨時的執行區,在這個區域裡不用再 call 物件本身就可以對物件執行動作,而 Scope Functions 總共有五個,以下會分別解釋這五個的差異:let
, run
, with
, apply
, and also
。
在什麼樣子的開發情境下會需要使用到呢?
val luffy = Character("Luffy", 20, "SKY PIEA")
luffy.moveTo("Wano Country")
luffy.incrementAge()
當同一個物件需要執行超過一個以上的動作時,為了讓程式碼看起來更簡潔,避免一直寫重複的名稱,這時候就可以依情境使用那 5 個 function。
Character("Luffy", 20, "SKY PIEA").apply {
moveTo("Wano Country")
incrementAge()
}
這是我看到某篇文章寫得口訣,我補上 with 在這段話中,讓 Scope Functions 使用方式搭配官方的表格更清晰。
Function | Object reference | Return value | Is extension function |
---|---|---|---|
let |
it |
Lambda result | Yes |
run |
this |
Lambda result | Yes |
run |
- | Lambda result | No: called without the context object |
with |
this |
Lambda result | No: takes the context object as an argument. |
apply |
this |
Context object | Yes |
also |
it |
Context object | Yes |
let
, run
, with
返回值都是最後一行apply
, also
返回值是物件本身? Object reference : An object reference is information on how to find a particular object
那針對使用情境官方有提供了一些方向做參考,但也沒有說這些情境下一定要使用特定的 function,主要還是依目的跟易讀為主,使用的人可以根據團隊或是專案立訂使用的情境:
let
let
apply
run
run
also
with
雖然他可以讓程式碼變得更簡潔,但要避免過度使用他而呈現巢狀,這會很容易混淆目前的開發內容是回傳 this 還是 it
我們要如何實作這 5 個 function 呢?以下我們有一個 Character 的 data class(有標註 // system display
都是 IDE 會顯示的,是為了凸顯 it 跟 this 所以有打出來)。
data class Character (
var name: String,
var age: Int,
var country: String
) {
fun moveTo(newCity: String) { country = newCity }
fun incrementAge() { age++ }
}
let 的 Object reference 是 it ,返回值是最後一行的 value:
val luffy = Character("Luffy", 20, "SKY PIEA").let { it: Character // system display
it.moveTo("Wano Country")
it.incrementAge()
it.age
}
print(luffy) // get 21
Object reference 的 it 也可以自行命名:
Character("Luffy", 20, "SKY PIEA").let { child ->
child.moveTo("Wano Country")
child.incrementAge()
child.age
}
run 的 Object reference 是 this,返回值是最後一行的 value,在 block 中可省略 this 直接呼叫物件執行動作:
val luffy = Character("Luffy", 20, "SKY PIEA").run { this: Character // system display
moveTo("Wano Country")
incrementAge()
age
}
print(luffy) // get 21
而 run 除了針對物件本身形成一個 scope 之外還有另一個用法,當我們上述的狀況有可能為 null 的情況下想要針對 null 與 non-null 做不同的行為:
val person: Character ?= null
// 一般方式
if (person.isNullOrEmpty()) {
// do something
} else {
// do something
}
// scope function
person?.let {
// only not null object can access
} ?: run {
// do something when object is null
}
這裡補充一個意外的小知識,原本只單純想補充 ?:
這個的意思,意外查到他命名的來歷:
?:
稱作 Elvis Operator,如果第一操作數求值為真則返回其值,否則返回第二操作數的值
埃爾維斯運算符得名於它的通常表示法?:
,相似於埃爾維斯·普雷斯利
(即「貓王」)的顏文字側臉的額發,或者其他角度看相遇於他的得意的笑臉。
最後用簡單的例子來說明他的用法及語意:
val result = valueA ?: valueB
// 當 valueA 不等於 null 的時候 result = valueA
// 當 valueA 等於 null 的時候 result = valueB
以前面 person 來說,當 person 是 null 的時候就執行 run
scope 的事情。
with 的 Object reference 是 this,返回值是最後一行的 value:
val luffy = Character("Luffy", 20, "SKY PIEA")
val age = with(luffy) { this: Character // system display
moveTo("Wano Country")
incrementAge()
age
}
print(age) // get 21
also 的 Object reference 是 it,返回值是自己:
val luffy = Character("Luffy", 20, "SKY PIEA").also { it: Character // system display
it.moveTo("Wano Country")
it.incrementAge()
it.age // 雖然最後一行是 age 但 print 出來會是物件本身
}
print(luffy) // get Character(name=Alice, age=21, city=London)
跟 let
依樣 it 可以自行改名
val luffy = Character("Luffy", 20, "SKY PIEA").also { student ->
student.moveTo("Wano Country")
student.incrementAge()
student.age
}
print(luffy) // get Character(name=Alice, age=21, city=London)
apply 的 Object reference 是 this,返回值是自己,在 block 中可省略 this 直接呼叫物件執行動作:
val luffy = Character("Luffy", 20, "SKY PIEA").apply { this: Character // system display
moveTo("Wano Country")
incrementAge()
age // 雖然最後一行是 age 但 print 出來會是物件本身
}
print(luffy) // get Character(name=Alice, age=21, city=London)
data class Character 有錯,沒有 class member city 存在;第一個函式無法 compile。
另外,關於 with 的用法也有點錯誤。
下面印出來的應該要是 Character object,而不是 21 才對
val luffy = Character("Luffy", 20, "SKY PIEA")
with(luffy) {
moveTo("Wano Country")
incrementAge()
age
}
print(luffy)
感謝指正~~~已更正,目前我在 with 使用情境比較少,如果有其他使用情境也請大大們提點,已更正成以下寫法:
val luffy = Character("Luffy", 20, "SKY PIEA")
val age = with(luffy) { this: Character // system display
moveTo("Wano Country")
incrementAge()
age
}
print(age) // get 21
這邊也補上 with 的 source code
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return receiver.block()
}